﻿using Jacobi.Vst.Framework;
using Jacobi.Vst.Core;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Aleator.Resources;
using Aleator.Helpers;

namespace Aleator.Core
{
    /// <summary>
    /// Rhythm (chord playing) phrase class
    /// </summary>
    public class RhythmPhrase : ConcretePhrase
    {
        #region Members

        private List<ConcreteNote> m_chordNotes;

        private int m_chordResolution;
        private int m_chordTotal;

        private bool m_rigid;

        #endregion

        #region Constructors

        /// <summary>
        /// Main constructor
        /// </summary>
        /// <param name="progression">Progression this phrase is being attachd to</param>
        /// <param name="basephrase">Original to phrase to use as template for copying</param>
        public RhythmPhrase(ConcreteProgression progression, RhythmPhrase basephrase = null)
        {
            m_progression = progression;

            Notes = new List<ConcreteNote>();
            m_chordNotes = new List<ConcreteNote>();

            if (basephrase == null)
            {
                ChordResolution = Session.RndGenerator.Next(1, 4);
                SetTotals();

                //50% of chords being quantized to quarter notes or eight notes
                if (Session.RndGenerator.Next(1, 3) == 1)
                    m_resolution = 1;
                else
                    m_resolution = 2;

                if (!m_rigid)
                {
                    m_noteDistribution = Distribution.Tight;
                    Tight();
                }
                else
                {
                    m_noteDistribution = Distribution.Rigid;
                    Rigid();
                }
            }
            else
            {
                ChordResolution = basephrase.m_chordResolution;
                CopyToPhrase((ConcretePhrase)basephrase);
            }

            Progression.Notes.AddRange(m_notes);
        }

        /// <summary>
        /// Rhythm phrase constructor allowing for variance from a progression group
        /// </summary>
        /// <param name="progression"></param>
        /// <param name="basephrase"></param>
        public RhythmPhrase(ConcreteProgression progression, int variance)
        {
            m_progression = progression;

            //If there is no basephrase, we are generating original note lists. Otherwise, initiate copying procedure
            if (!progression.Composition.Progressions.Any())
            {
                throw new NotImplementedException("Variance requires at least one progression in the current composition!");
            }
            else
                Commonality.Explode(this);

            Progression.Notes.AddRange(m_notes);
        }
        #endregion

        #region Properties

        /// <summary>
        /// For chords, resolution is how many plays per whole note
        /// </summary>
        public int ChordResolution
        {
            get
            {
                return m_chordResolution;
            }
            set
            {
                switch (value)
                {
                    case 3:
                        m_chordResolution = 4;
                        break;
                    default:
                        m_chordResolution = value;
                        break;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Set total number of notes in the list
        /// </summary>
        /// <param name="sigma">Standard deviation for chord total</param>
        protected override void SetTotals(double sigma = 2.0)
        {
            if (Session.RndGenerator.Next(1, 3) == 1)
            {
                m_rigid = false;
                base.SetTotals();
                m_chordTotal = GetTotal(m_mu, m_sigma);
            }
            else
                m_rigid = true;

            m_noteOnMessageNumber = Convert.ToInt16(AppResources.RhythmOnMessageID);
            m_noteOffMessageNumber = Convert.ToInt16(AppResources.RhythmOffMessageID);
        }

        /// <summary>
        /// 
        /// </summary>
        protected override void Tight()
        {
            m_continuous.Beta = PlayableNotesInMeasure - 1;
            m_continuous.Alpha = 0;

            m_rayleigh.Sigma = 3.0;

            //inner counter will be re-used to add remaining notes into the phrase below
            int chordsInMeasure = m_chordTotal / m_progression.TotalMeasures;

            if (chordsInMeasure > PlayableNotesInMeasure)
                chordsInMeasure = PlayableNotesInMeasure;

            int j = 0;
            int beatbase = 0;
            int counter = 0;

            Chord c;

            ConcreteNote n = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote o = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote p = (ConcreteNote)m_notefactory.GetNote();

            m_measure = 0;

            //Fill out the first measure of the phrase
            while (j < chordsInMeasure)
            {
                GetBeat();

                //If the number returned by the generator is within the desired range and has not already been selected,
                //We can add it it to the note array
                if (!m_notes.Where(k => k != null && k.Location == m_location && k.Measure == 0).Any())
                {
                    //Pick a note from the appropriate chord in the phrase
                    //TODO: Make sure we are accounting for octaves - we'll stay within 3 to mimic the bass guitar
                    c = Progression.Chords.Where(ch => ch.Key <= m_location / m_resolution).OrderByDescending(ch => ch.Key)
                        .Select(ch => ch.Value).First();

                    m_chordNotes.Add(c.Notes.Where(k => c.Roots.Contains(k)).First());
                    m_chordNotes.Add(c.Notes.Where(k => c.Thirds.Contains(k)).First());
                    m_chordNotes.Add(c.Notes.Where(k => c.Fifths.Contains(k)).First());

                    n = m_chordNotes[0];
                    o = m_chordNotes[1];
                    p = m_chordNotes[2];

                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, 0, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, 0, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, 0, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                    Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, m_measure, m_location));

                    m_chordNotes.Clear();

                    j++;
                }
            }

            //Fill out the rest
            for (int i = 1; i < m_progression.TotalMeasures; i++)
            {
                beatbase = i * m_progression.Numerator;

                if (j > m_progression.TotalMeasures * chordsInMeasure)
                    break;
                try
                {
                    for (int k = 0; k < chordsInMeasure; k++)
                    {
                        //WTF is this.
                        if (k == 0)
                            m_location = m_notes[0].Location;
                        else
                            m_location = m_notes[(k * 3)].Location;

                        c = Progression.Chords.Where(ch => ch.Key <= beatbase + (m_location / m_resolution)).OrderByDescending(ch => ch.Key)
                            .Select(ch => ch.Value).First();

                        m_chordNotes.Add(c.Notes.Where(l => c.Roots.Contains(l)).First());
                        m_chordNotes.Add(c.Notes.Where(l => c.Thirds.Contains(l)).First());
                        m_chordNotes.Add(c.Notes.Where(l => c.Fifths.Contains(l)).First());

                        n = m_chordNotes[0];
                        o = m_chordNotes[1];
                        p = m_chordNotes[2];

                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, i, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, i, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, i, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                        Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, i, m_location));

                        m_chordNotes.Clear();

                        j++;
                    }
                }
                catch (IndexOutOfRangeException ior)
                {
                    Debug.WriteLine(ior.Message);
                }
            }

            //Get any beat that isn't in the kick collection yet
            List<int> beats = new List<int>();
            List<ConcreteNote> fullnotes = m_notes.Where(k => k != null).ToList();

            try
            {
                for (int i = 0; i < PlayableNotesInMeasure; i++)
                {
                    if (fullnotes.Where(k => k.Location == i).Count() == 0)
                        beats.Add(i);
                }
            }
            catch (NullReferenceException nre)
            {
                Debug.WriteLine("Error message: " + nre.Message);
            }

            if (beats.Count > 0)
            {
                m_location = beats[Session.RndGenerator.Next(beats.Count)];

                //Add the remaining notes to the array to fill it up using the same process
                while (j < m_noteTotal)
                {
                    m_measure = Session.RndGenerator.Next(m_progression.TotalMeasures);

                    if (counter == m_progression.TotalMeasures)
                    {
                        counter = 0;
                        m_location = beats[Session.RndGenerator.Next(beats.Count)];
                    }

                    if (!m_notes.Where(k => k != null && k.Location == m_location && k.Measure == m_measure).Any())
                    {
                        beatbase = m_measure * m_progression.Numerator;

                        c = Progression.Chords.Where(ch => ch.Key <= beatbase + (m_location / m_resolution)).OrderByDescending(ch => ch.Key)
                            .Select(ch => ch.Value).First();

                        m_chordNotes.Add(c.Notes.Where(l => c.Roots.Contains(l)).First());
                        m_chordNotes.Add(c.Notes.Where(l => c.Thirds.Contains(l)).First());
                        m_chordNotes.Add(c.Notes.Where(l => c.Fifths.Contains(l)).First());

                        n = m_chordNotes[0];
                        o = m_chordNotes[1];
                        p = m_chordNotes[2];

                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(m_location, m_measure, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                        Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, m_measure, m_location));

                        m_chordNotes.Clear();

                        j++;
                    }

                    counter++;
                }
            }

            AddRests();
        }
        
        /// <summary>
        /// This method will produce a chord phrase that will play on almost every quantized note. Limited variation in notes
        /// </summary>
        protected override void Rigid()
        {
            //First we need to determine where the missing notes fall
            int missingbeat = Session.RndGenerator.Next(PlayableNotesInMeasure);

            Chord c;
            Chord.ScaleDegree degree = Chord.ScaleDegree.I;

            ConcreteNote n = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote o = (ConcreteNote)m_notefactory.GetNote();
            ConcreteNote p = (ConcreteNote)m_notefactory.GetNote();

            int beatbase;

            //Iterate through all the measures
            for (int m = 0; m < Progression.TotalMeasures; m++)
            {
                beatbase = m_progression.Numerator * m;

                //Iterate through the playable notes in a measure
                for (int b = 0; b < PlayableNotesInMeasure; b++)
                {
                    //If this beat/measure combo wasnt selected for omission, we can add it
                    if (missingbeat != b)
                    {
                        c = Progression.Chords.Where(ch => ch.Key <= beatbase + (b / m_resolution)).OrderByDescending(ch => ch.Key)
                            .Select(ch => ch.Value).First();

                        //Only change notes if we are changing chords as well
                        if (c.Degree != degree)
                        {
                            m_chordNotes.Add(c.Notes.Where(k => c.Roots.Contains(k)).First());
                            m_chordNotes.Add(c.Notes.Where(k => c.Thirds.Contains(k)).First());
                            m_chordNotes.Add(c.Notes.Where(k => c.Fifths.Contains(k)).First());

                            n = m_chordNotes[0];
                            o = m_chordNotes[1];
                            p = m_chordNotes[2];

                            ChordInterval = c.GetChordInterval(n);

                            m_chordNotes.Clear();
                        }

                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(b, m, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(b, m, o, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(b, m, p, m_noteOnMessageNumber, m_noteOffMessageNumber, this, c.DisplayName));

                        Debug.WriteLine(string.Format("Adding {0} chord: {1},{2},{3} | Measure: {4}, Location: {5}", c.DisplayName, n.Name, o.Name, p.Name, m, b));

                        degree = c.Degree;
                    }
                }
            }

            AddRests();
        }

        /// <summary>
        /// Insert rests into the progression by setting the noteoff locations for all notes
        /// </summary>
        protected override void AddRests()
        {
            //Sort note list (sort is done by beat value)
            Notes.Sort();

            //Group the note list by location
            var result = Notes.GroupBy(a => a.Location);

            int total = result.Count();
            int resultcounter = 0;
            int nextlocation;
            int off;
            int spacer;

            //For each group, randomly pick a number less than the gap value and play the notes for that duration
            foreach (var group in result)
            {
                //Get the location of the next group and set the offset accordingly
                if (resultcounter < result.Count() - 1)
                {
                    var nextGroup = result.ElementAt(resultcounter + 1);
                    nextlocation = nextGroup.ElementAt(0).Location;
                    spacer = nextlocation - group.ElementAt(0).Location;
                }
                else
                {
                    nextlocation = result.ElementAt(0).ElementAt(0).Location;
                    spacer = (PlayableNotesInMeasure - group.ElementAt(0).Location) + nextlocation;
                }

                off = Session.RndGenerator.Next(1, spacer);

                //Go through all of the notes with this gap length and set the off note values
                foreach (ConcreteNote n in group)
                {
                    n.OffLocation = n.Location + off;

                    //If the offbeat to the appropriate beat in the next measure. Otherwise the note is turned off in the same measure.
                    if (n.OffLocation >= PlayableNotesInMeasure - 1)
                    {
                        if (n.Measure == Progression.TotalMeasures - 1)
                        {
                            n.OffLocation = PlayableNotesInMeasure - 1;
                            n.OffMeasure = n.Measure;
                        }
                        else
                        {
                            n.OffLocation -= PlayableNotesInMeasure - 1;
                            n.OffMeasure = n.Measure + 1;
                        }
                    }
                    else
                        n.OffMeasure = n.Measure;
                }

                resultcounter++;
            }
        }

        #endregion
    }
}
